import os
import itertools
import traceback
import datetime
from collections import OrderedDict

import pandas
import numpy
import numba

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt

from pandas.plotting import register_matplotlib_converters

register_matplotlib_converters()
from coin.pnl.sim_stat_plot import plot_orderbin
import coin.pnl.sim_stat_calc2 as sscalc2
import coin.proto.coin_order_enums_pb2 as order_enums
from coin.exchange.kr_rest.product_holders import get_holder_from_product
from coin.exchange.kr_rest.product.product_impl import generate_product_from_str2

EPS = 1e-6


class OrderPlotSetting(object):
  def __init__(self, filename_prefix, time_range):
    self.filename_prefix = filename_prefix
    self.time_range = time_range


def set_xlim_time(start_time, end_time):
  send = pandas.DatetimeIndex([start_time, end_time])
  plt.xlim(send[0], send[1])
  return send[0], send[1]


def get_table_res(df):
  def get_interval_stat(x, ts, method, interval_sec=60):
    sr = pandas.Series(x, index=pandas.to_datetime(ts))
    resampled_data = getattr(sr.resample(f"{interval_sec}S"),
                             method)().ffill().fillna(0.0).to_numpy()
    return resampled_data

  copied_df = df.copy()
  # apply interval-sliced series
  for tholds, rows in copied_df.iterrows():
    ts_arr = rows['ts']
    for stat, sr in rows.items():
      if stat == 'ts':
        continue
      if stat in [
          'uptime_res_list', 'tight_uptime_res_list', 'tight_sell_depth_list',
          'tight_buy_depth_list', 'sell_depth_list', 'buy_depth_list'
      ]:
        copied_df.loc[tholds, stat] = get_interval_stat(sr, ts_arr, 'max')
      else:
        interval_stat = get_interval_stat(sr, ts_arr, 'min')
        numpy.place(interval_stat, interval_stat == 0.0, numpy.nan)
        copied_df.loc[tholds, stat] = interval_stat
  apply_map = OrderedDict([('Avg', lambda x: numpy.nanmean(x) if len(x) > 0 else numpy.nan),
                           ('Min', lambda x: numpy.nanmin(x) if len(x) > 0 else numpy.nan),
                           ('Max', lambda x: numpy.nanmax(x) if len(x) > 0 else numpy.nan)])
  name_map = OrderedDict([('uptime_res_list', 'Uptime'), ('tight_uptime_res_list', 'Tight.Uptime'),
                          ('sell_spreads_list', 'Sell.Spread'), ('buy_spreads_list', 'Buy.Spread'),
                          ('tight_sell_depth_list', 'Tight.Sell.Depth($)'),
                          ('tight_buy_depth_list', 'Tight.Buy.Depth($)'),
                          ('sell_depth_list', 'Sell.Depth($)'), ('buy_depth_list', 'Buy.Depth($)')])
  res = OrderedDict()
  for k, v in apply_map.items():
    tmp = copied_df.applymap(v).rename(columns=name_map)
    res[k] = tmp
  final_res = pandas.concat(res, names=['Stat'])
  table_df = final_res.reset_index()[[
      'price_bp_thold',
      'quote_thold',
      'Stat',
  ] + list(name_map.values())]

  table_df.loc[table_df['Stat'].isin(['Max', 'Min']), ['Uptime', 'Tight.Uptime']] = numpy.nan
  table_df[['Uptime', 'Tight.Uptime', 'Sell.Spread', 'Buy.Spread'
           ]] = table_df[['Uptime', 'Tight.Uptime', 'Sell.Spread', 'Buy.Spread'
                         ]].apply(lambda series: series.apply(lambda value: f"{value:,.2%}"
                                                              if not numpy.isnan(value) else "N/A"))
  table_df[['Sell.Depth($)', 'Buy.Depth($)', 'Tight.Sell.Depth($)',
            'Tight.Buy.Depth($)']] = table_df[[
                'Sell.Depth($)', 'Buy.Depth($)', 'Tight.Sell.Depth($)', 'Tight.Buy.Depth($)'
            ]].apply(lambda series: series.apply(lambda value: f"{value:,.0f}"
                                                 if not numpy.isnan(value) else "N/A"))
  table_df['Stat'] = pandas.Categorical(table_df['Stat'], list(apply_map.keys()))
  table_df.sort_values(['price_bp_thold', 'quote_thold', 'Stat'], inplace=True)
  return table_df


def plot_mm_stats_chart(final_res,
                        bbodf,
                        oedf,
                        out_dir,
                        file_name,
                        is_split=True,
                        is_pnl_plot=False):
  symbol, file_name = file_name.split(':')
  final_res = final_res.copy()
  copied_bbodf = bbodf.copy()
  copied_bbodf.drop_duplicates(inplace=True)
  copied_bbodf[
      'timestamp'] = copied_bbodf['book_timestamp'] if is_pnl_plot else copied_bbodf['timestamp']

  filldf = oedf.loc[~numpy.isnan(oedf['fill_price'])]
  fmixdf = pandas.concat([filldf, bbodf[['timestamp', 'mid_price']]], axis=0,
                         sort=False).sort_values('timestamp')
  fmixdf['mid_price'] = fmixdf['mid_price'].fillna(method='ffill')
  fmixdf = fmixdf.loc[fmixdf['fill_price'] > 0]
  fmixdf = fmixdf.loc[~numpy.isnan(fmixdf['fill_price'])]
  fmixdf = fmixdf.loc[~numpy.isnan(fmixdf['mid_price'])]
  fillp2mid = ((fmixdf['fill_price'] / fmixdf['mid_price']) - 1) * 1e4
  fmixdti = fmixdf['timestamp']
  fillprice = fmixdf['fill_price']

  for params, row in final_res.iterrows():
    param_names = "_".join(
        [f'{name}_{params[idx]}' for idx, name in enumerate(final_res.index.names)])
    os.makedirs(f'{out_dir}/{param_names}', exist_ok=True)
    print(f'start plotting: {param_names}....')
    ts_list = row['ts']

    uptime_sr = pandas.Series(row['uptime_res_list'], index=ts_list)
    tight_uptime_sr = pandas.Series(row['tight_uptime_res_list'], index=ts_list)
    tight_sell_depth_sr = pandas.Series(row['tight_sell_depth_list'], index=ts_list)
    tight_buy_depth_sr = pandas.Series(row['tight_buy_depth_list'], index=ts_list)
    sell_depth_sr = pandas.Series(row['sell_depth_list'], index=ts_list)
    buy_depth_sr = pandas.Series(row['buy_depth_list'], index=ts_list)
    sell_spread_sr = pandas.Series(row['sell_spreads_list'], index=ts_list)
    buy_spread_sr = pandas.Series(row['buy_spreads_list'], index=ts_list)

    copied_bbodf.ffill(inplace=True)
    copied_bbodf['market_bid_spread'] = (copied_bbodf['mid_price'] -
                                         copied_bbodf['bid0_price']) / copied_bbodf['mid_price']
    copied_bbodf['market_ask_spread'] = (copied_bbodf['ask0_price'] -
                                         copied_bbodf['mid_price']) / copied_bbodf['mid_price']

    rolling_length_hour = 3  # rolling uptime calc period
    tight_uptime_length_sr = tight_uptime_sr.replace(False, True)
    tight_uptime_length_sr.index = pandas.to_datetime(tight_uptime_length_sr.index)
    tight_uptime_sr.index = pandas.to_datetime(tight_uptime_sr.index)

    tight_uptime_calc_res = tight_uptime_sr.rolling(
        window=f'{rolling_length_hour * 60 * 60}S',
        min_periods=10).sum() / tight_uptime_length_sr.rolling(
            window=f'{rolling_length_hour * 60 * 60}S', min_periods=10).sum()

    tight_uptime_calc_res.index = tight_uptime_calc_res.index.astype(int)

    uptime_length_sr = uptime_sr.replace(False, True)
    uptime_length_sr.index = pandas.to_datetime(uptime_length_sr.index)
    uptime_sr.index = pandas.to_datetime(uptime_sr.index)

    uptime_calc_res = uptime_sr.rolling(window=f'{rolling_length_hour * 60 * 60}S',
                                        min_periods=10).sum() / uptime_length_sr.rolling(
                                            window=f'{rolling_length_hour * 60 * 60}S',
                                            min_periods=10).sum()

    uptime_calc_res.index = uptime_calc_res.index.astype(int)
    if len(ts_list) == 0:
      date_range = pandas.date_range(
          pandas.Timestamp(copied_bbodf['timestamp'].min()).strftime('%Y%m%d'),
          pandas.Timestamp(copied_bbodf['timestamp'].max()).strftime('%Y%m%d'))
    else:
      date_range = pandas.date_range(
          pandas.Timestamp(max(ts_list[0], copied_bbodf['timestamp'].min())).strftime('%Y%m%d'),
          pandas.Timestamp(min(ts_list[-1], copied_bbodf['timestamp'].max())).strftime('%Y%m%d'))
    if len(date_range) <= 7:
      split_hours = 6
      split_date_range = pandas.date_range(date_range[0],
                                           date_range[-1] + pandas.Timedelta(days=1),
                                           freq=f'{split_hours}h')[:-1]
    else:
      split_hours = 24
      split_date_range = date_range
    for start_date in split_date_range:
      end_date = start_date + pandas.Timedelta(
          hours=split_hours) if is_split else date_range[-1] + pandas.Timedelta(days=1)
      tmp_array = numpy.array(ts_list)
      tmp_array_2 = copied_bbodf['timestamp'].to_numpy()
      inside_dt = ((tmp_array >= int(start_date.timestamp() * 1e9)) &
                   (tmp_array <= int(end_date.timestamp() * 1e9)))
      sliced_dt = tmp_array[inside_dt]
      sliced_dt_2 = tmp_array_2[(tmp_array_2 >= int(start_date.timestamp() * 1e9)) &
                                (tmp_array_2 <= int(end_date.timestamp() * 1e9))]
      fig, axes = plt.subplots(5, figsize=(20, 15))
      fig.suptitle(
          f"{symbol} {param_names}\n"\
          f"{start_date.strftime('%Y-%m-%d %H:%M:%S')}-{end_date.strftime('%Y-%m-%d %H:%M:%S')}",
          fontsize=10)
      axes[0].plot(pandas.DatetimeIndex(sliced_dt),
                   uptime_calc_res.loc[sliced_dt].values,
                   'b',
                   drawstyle='steps-post',
                   label=f'Uptime (Rolling {rolling_length_hour}h)')
      axes[0].plot(pandas.DatetimeIndex(sliced_dt),
                   tight_uptime_calc_res.loc[sliced_dt].values,
                   'c',
                   drawstyle='steps-post',
                   label=f'Tight Uptime (Rolling {rolling_length_hour}h)')
      axes[0].set_xlim(start_date, end_date)
      axes[0].legend(loc='center left',
                     prop={'size': 'x-small'},
                     bbox_to_anchor=(1.0005, 0.5),
                     borderaxespad=0)

      axes[1].plot(pandas.DatetimeIndex(sliced_dt),
                   -tight_buy_depth_sr.loc[sliced_dt].values,
                   'g',
                   lw=0.5,
                   label='Avg.Tight.BuyDepth($)')
      axes[1].fill_between(pandas.DatetimeIndex(sliced_dt),
                           -tight_buy_depth_sr.loc[sliced_dt].values,
                           0,
                           facecolor='g',
                           alpha=1)
      axes[1].plot(pandas.DatetimeIndex(sliced_dt),
                   tight_sell_depth_sr.loc[sliced_dt].values,
                   'r',
                   lw=0.5,
                   label='Avg.Tight.SellDepth($)')
      axes[1].fill_between(pandas.DatetimeIndex(sliced_dt),
                           tight_sell_depth_sr.loc[sliced_dt].values,
                           0,
                           facecolor='r',
                           alpha=1)
      axes[1].plot(pandas.DatetimeIndex(sliced_dt),
                   -buy_depth_sr.loc[sliced_dt].values,
                   color='y',
                   linestyle='dashed',
                   lw=0.5,
                   alpha=0.3,
                   label='Avg.BuyDepth($)')
      axes[1].fill_between(pandas.DatetimeIndex(sliced_dt),
                           -buy_depth_sr.loc[sliced_dt].values,
                           0,
                           facecolor='y',
                           alpha=0.3)
      axes[1].plot(pandas.DatetimeIndex(sliced_dt),
                   sell_depth_sr.loc[sliced_dt].values,
                   color='b',
                   linestyle='dashed',
                   lw=0.2,
                   alpha=0.3,
                   label='Avg.SellDepth($)')
      axes[1].fill_between(pandas.DatetimeIndex(sliced_dt),
                           sell_depth_sr.loc[sliced_dt].values,
                           0,
                           facecolor='b',
                           alpha=0.3)

      axes[1].set_xlim(start_date, end_date)
      axes[1].legend(loc='center left',
                     prop={'size': 'x-small'},
                     bbox_to_anchor=(1.0005, 0.5),
                     borderaxespad=0)
      sliced_bbodf = copied_bbodf.loc[copied_bbodf['timestamp'].isin(sliced_dt_2)]

      axes[2].plot(pandas.DatetimeIndex(sliced_dt),
                   -buy_spread_sr.loc[sliced_dt].values * 1e4,
                   'g.',
                   markersize=0.25,
                   label='Avg.BuySpread(bp)')
      axes[2].plot(pandas.DatetimeIndex(sliced_dt),
                   sell_spread_sr.loc[sliced_dt].values * 1e4,
                   'r.',
                   markersize=0.25,
                   label='Avg.SellSpread(bp)')

      axes[2].plot(pandas.DatetimeIndex(sliced_bbodf['timestamp'].to_numpy()),
                   -sliced_bbodf['market_bid_spread'].values * 1e4,
                   'c:',
                   lw=0.2,
                   drawstyle='steps-post',
                   label='Market.BidSpread(bp)')
      axes[2].plot(pandas.DatetimeIndex(sliced_bbodf['timestamp'].to_numpy()),
                   sliced_bbodf['market_ask_spread'].values * 1e4,
                   'm:',
                   lw=0.2,
                   drawstyle='steps-post',
                   label='Market.AskSpread(bp)')
      fidxbuy = (fmixdf['sign'] == 1) & (fmixdf['timestamp'] >= sliced_dt_2.min()) & (
          fmixdf['timestamp'] <= sliced_dt_2.max())
      fidxsell = (fmixdf['sign'] == -1) & (fmixdf['timestamp'] >= sliced_dt_2.min()) & (
          fmixdf['timestamp'] <= sliced_dt_2.max())
      axes[2].plot(fmixdti[fidxbuy],
                   fillp2mid[fidxbuy],
                   'g.',
                   fmixdti[fidxsell],
                   fillp2mid[fidxsell],
                   'r.',
                   markersize=2)

      axes[2].set_xlim(start_date, end_date)
      axes[2].legend(loc='center left',
                     prop={'size': 'x-small'},
                     bbox_to_anchor=(1.0005, 0.5),
                     borderaxespad=0)

      axes[3].plot(pandas.DatetimeIndex(sliced_bbodf['timestamp'].to_numpy()),
                   sliced_bbodf['bid0_price'].to_numpy(),
                   'g',
                   lw=0.1,
                   drawstyle='steps-post',
                   label='Market Bid')
      axes[3].plot(pandas.DatetimeIndex(sliced_dt_2),
                   sliced_bbodf['ask0_price'].to_numpy(),
                   'r',
                   lw=0.1,
                   drawstyle='steps-post',
                   label='Market Ask')
      axes[3].plot(pandas.DatetimeIndex(sliced_dt_2),
                   sliced_bbodf['mid_price'].to_numpy(),
                   'k',
                   lw=0.1,
                   drawstyle='steps-post',
                   label='Market MidP')
      axes[3].plot(fmixdti[fidxbuy],
                   fillprice[fidxbuy],
                   'g.',
                   fmixdti[fidxsell],
                   fillprice[fidxsell],
                   'r.',
                   markersize=2)

      axes[3].set_xlim(start_date, end_date)
      axes[3].legend(loc='center left',
                     prop={'size': 'x-small'},
                     bbox_to_anchor=(1.0005, 0.5),
                     borderaxespad=0)
      axes[4].axis('tight')
      axes[4].axis('off')
      sliced_res = final_res.applymap(lambda x: numpy.array(x)[inside_dt])
      table_df = get_table_res(sliced_res)
      table_df = table_df[(table_df[final_res.index.names] == params).all(axis=1)].reset_index()
      bold_idx = table_df[table_df['Stat'] == 'Avg'].index
      table = axes[4].table(cellText=table_df.to_numpy(),
                            colLabels=table_df.columns,
                            loc='center',
                            cellLoc='center')
      for key, cell in table.get_celld().items():
        row, _ = key
        if row - 1 in bold_idx:
          cell.set_text_props(fontproperties=matplotlib.font_manager.FontProperties(weight='bold'))
      table.auto_set_column_width(col=list(range(len(table_df.columns))))

      if is_split:
        fig.savefig(
            f'{out_dir}/{param_names}/{symbol}_{file_name}_{start_date.strftime("%Y%m%d.%H.%M")}-{end_date.strftime("%Y%m%d.%H.%M")}.png',
            dpi=300)
        print(
            f'{out_dir}/{param_names}/{symbol}_{file_name}_{start_date.strftime("%Y%m%d.%H.%M")}-{end_date.strftime("%Y%m%d.%H.%M")}.png Done!'
        )
        plt.close(fig)
      else:
        fig.savefig(
            f'{out_dir}/{param_names}_{symbol}_{file_name}_{start_date.strftime("%Y%m%d.%H.%M")}-{end_date.strftime("%Y%m%d.%H.%M")}.png',
            dpi=300)
        print(
            f'{out_dir}/{param_names}_{symbol}_{file_name}_{start_date.strftime("%Y%m%d.%H.%M")}-{end_date.strftime("%Y%m%d.%H.%M")}.png Done!'
        )
        plt.close(fig)
        break
    print('===============================================================\n')


def plot_ordpos(startts, endts, bbodf, posts, poss, prcdict, tag=None):
  if startts is not None and endts is not None:
    tss = pandas.DatetimeIndex(bbodf['timestamp']).astype(int)
    bboidx = ((tss >= startts) & (tss <= endts))
    bbodf = bbodf.loc[bboidx]
  plt.plot(pandas.DatetimeIndex(bbodf['timestamp']).to_numpy(),
           bbodf['bid0_price'].to_numpy(),
           'c:',
           pandas.DatetimeIndex(bbodf['timestamp']).to_numpy(),
           bbodf['ask0_price'].to_numpy(),
           'm:',
           lw=0.2,
           drawstyle='steps-post')
  set_xlim_time(startts, endts)
  plt.ylabel("price")
  plot_orderbin(**prcdict, start_ts=startts, end_ts=endts, tag=tag)


def plot_edges(oedf, bbodf, startts, endts):
  cpdf = oedf.copy()
  cpdf.loc[cpdf['type'] == 'CANCEL_CONFIRMED', 'order_price'] = -1
  cpdf.loc[cpdf['type'] == 'ORDER_FILLED', 'order_price'] = -1
  cpdf.loc[cpdf['type'] == 'ORDER_ERROR', 'order_price'] = -1
  dts = pandas.DatetimeIndex([startts, endts]).astype(int)
  startts = dts[0]
  endts = dts[1]

  for sign, marker in [(1, 'g-'), (-1, 'r-')]:
    mixdf = pandas.concat([cpdf.loc[cpdf['sign'] == sign], bbodf[['timestamp', 'mid_price']]],
                          axis=0,
                          sort=False).sort_values('timestamp').ffill()
    mixdf = mixdf.loc[(startts < mixdf['timestamp']) & (mixdf['timestamp'] < endts)]
    mixdf.loc[mixdf['order_price'] == -1] = numpy.nan
    op2mid = (mixdf['order_price'] / mixdf['mid_price']).to_numpy()
    op2mid = (op2mid - 1) * 1e4
    mxdti = pandas.DatetimeIndex(mixdf['timestamp']).to_numpy()
    plt.plot(mxdti, op2mid, marker, lw=0.5, alpha=0.5)
  plt.legend(['buyp - midp(bp)', 'sellp - midp(bp)'], loc='upper right')

  filldf = oedf.loc[~numpy.isnan(oedf['fill_price'])]
  fmixdf = pandas.concat([filldf, bbodf[['timestamp', 'mid_price']]], axis=0,
                         sort=False).sort_values('timestamp')
  fmixdf['mid_price'] = fmixdf['mid_price'].fillna(method='ffill')
  fmixdf = fmixdf.loc[fmixdf['fill_price'] > 0]
  fmixdf = fmixdf.loc[~numpy.isnan(fmixdf['fill_price'])]
  fmixdf = fmixdf.loc[~numpy.isnan(fmixdf['mid_price'])]
  fillp2mid = ((fmixdf['fill_price'] / fmixdf['mid_price']) - 1) * 1e4
  fidxbuy = fmixdf['sign'] == 1
  fidxsell = fmixdf['sign'] == -1
  fmixdti = pandas.DatetimeIndex(fmixdf['timestamp']).to_numpy()
  plt.plot(fmixdti[fidxbuy],
           fillp2mid[fidxbuy].to_numpy(),
           'g.',
           fmixdti[fidxsell],
           fillp2mid[fidxsell].to_numpy(),
           'r.',
           markersize=2)


def plot_mm_stats_table(mea,
                        symbol,
                        oedf,
                        bbodf,
                        price_bp_tholds,
                        quote_tholds,
                        base_tholds,
                        is_pnl_plot=False):

  unituple_float64_2 = numba.types.containers.UniTuple(numba.float64, 2)

  @numba.guvectorize([(numba.float64[:, :], numba.int64[:, :], numba.int64[:], numba.int64,
                       numba.float64, numba.float64[:])],
                     '(m, n), (m, p), (q), (), ()->(q)',
                     target='parallel',
                     cache=True)
  def get_depth(wo_array, ts_id_type_array, ts_array, sign, quote_thold, res):
    """
    wo_array: (m, n) array, m is the number of ORDER_EVENT, n is ['order_qty', 'order_price', 'fill_qty', 'price_thold']
    ts_id_type_array: (m, p) array, m is the number of ORDER_EVENT, p is ['timestamp', 'order_id', 'type']
    ts_array: (q) array, q is the number of unique timestamps
    sign: int, 1 for buy, -1 for sell
    quote_thold: float, quote threshold
    res: (q) array, q is the number of unique timestamps
    """
    # {id: (price, qty)}
    id_order_dict = numba.typed.Dict.empty(key_type=numba.int64, value_type=unituple_float64_2)
    depth = 0.0
    res[:] = numpy.zeros(ts_array.shape[0], dtype=numpy.float64)
    # first order added
    assert ts_id_type_array[0, 2] == 0 or ts_id_type_array[
        0, 2] == 3, 'First ORDER_EVENT type must be ORDER_ACCEPTED'
    # Current Order Tracking
    if ts_id_type_array[0, 2] == 0:
      id_order_dict[ts_id_type_array[0, 1]] = (wo_array[0, 1], wo_array[0, 0])
      working_amount = wo_array[0, 1] * wo_array[0, 0]
      if wo_array[0, 1] * sign >= wo_array[0, 3] * sign and working_amount >= quote_thold:
        depth += working_amount
    res[0] = depth
    ts_idx = 0
    # Loop
    for i in range(1, wo_array.shape[0]):
      order_qty, order_price, fill_qty, pthold = wo_array[i, :]
      working_amount = order_qty * order_price
      order_id, order_type = ts_id_type_array[i, 1:]
      depth = 0.0
      # 1. Check Current ORDER_EVENT info
      if order_type == 0:  # ORDER_ACCEPTED
        id_order_dict[order_id] = (order_price, order_qty)
      elif order_type == 1:  # ORDER_FILLED
        p, q = id_order_dict.get(order_id, (0.0, 0.0))
        if p != 0.0 and q != 0.0:
          if q - fill_qty <= 1.0:
            id_order_dict.pop(order_id, None)
          else:
            id_order_dict[order_id] = (p, q - fill_qty)
      elif order_type == 2:  # ORDER_CANCELLED
        id_order_dict.pop(order_id, None)

      # 2. Tight Working Order checking
      for _, (order_price, order_qty) in id_order_dict.items():
        working_amount = order_price * order_qty
        if order_price * sign >= pthold * sign and working_amount >= quote_thold:
          depth += working_amount

      if ts_id_type_array[i, 0] == ts_array[ts_idx]:
        res[ts_idx] = depth
      else:
        ts_idx += 1
        res[ts_idx] = depth

  @numba.guvectorize([(numba.float64[:, :], numba.int64[:, :], numba.int64[:], numba.int64,
                       numba.float64, numba.float64[:])],
                     '(m, n), (m, p), (q), (), ()->(q)',
                     target='parallel',
                     cache=True)
  def get_spread(wo_array, ts_id_type_array, ts_array, sign, quote_thold, res):
    """
    wo_array: (m, n) array, m is the number of ORDER_EVENT, n is ['order_qty', 'order_price', 'fill_qty', 'price_thold', 'mid_price']
    ts_id_type_array: (m, p) array, m is the number of ORDER_EVENT, p is ['timestamp', 'order_id', 'type']
    ts_array: (q) array, q is the number of unique timestamps
    sign: int, 1 for buy, -1 for sell
    quote_thold: float, quote threshold
    res: (q) array, q is the number of unique timestamps
    """
    # {id: (price, qty)}
    id_order_dict = numba.typed.Dict.empty(key_type=numba.int64, value_type=unituple_float64_2)
    working_id_order_dict = numba.typed.Dict.empty(key_type=numba.int64,
                                                   value_type=unituple_float64_2)

    spread = 0.0
    res[:] = numpy.zeros(ts_array.shape[0], dtype=numpy.float64)
    # first order added
    assert ts_id_type_array[0, 2] == 0 or ts_id_type_array[
        0, 2] == 3, 'First ORDER_EVENT type must be ORDER_ACCEPTED'
    # Current Order Tracking
    if ts_id_type_array[0, 2] == 0:
      id_order_dict[ts_id_type_array[0, 1]] = (wo_array[0, 1], wo_array[0, 0])
      working_amount = wo_array[0, 1] * wo_array[0, 0]
      if wo_array[0, 1] * sign >= wo_array[0, 3] * sign and working_amount >= quote_thold:
        spread = (wo_array[0, 4] - wo_array[0, 1]) * sign / wo_array[0, 4]
        working_id_order_dict[ts_id_type_array[0, 1]] = (wo_array[0, 1], wo_array[0, 0])
      else:
        spread = numpy.nan
    res[0] = spread
    ts_idx = 0
    # Loop
    for i in range(1, wo_array.shape[0]):
      order_qty, order_price, fill_qty, pthold, mid_price = wo_array[i, :]
      working_amount = order_qty * order_price
      order_id, order_type = ts_id_type_array[i, 1:]
      min_spread = numpy.nan
      # 1. Check Current ORDER_EVENT info
      if order_type == 0:  # ORDER_ACCEPTED
        id_order_dict[order_id] = (order_price, order_qty)

      elif order_type == 1:  # ORDER_FILLED
        p, q = id_order_dict.get(order_id, (0.0, 0.0))
        if p != 0.0 and q != 0.0:
          if q - fill_qty <= 0.0:
            id_order_dict.pop(order_id, None)
          else:
            id_order_dict[order_id] = (p, q - fill_qty)

      elif order_type == 2:  # ORDER_CANCELLED
        id_order_dict.pop(order_id, None)

      # 2. Tight Working Order checking
      for _, (order_price, order_qty) in id_order_dict.items():
        working_amount = order_price * order_qty
        if order_price * sign >= pthold * sign and working_amount >= quote_thold:
          spread = (mid_price - order_price) * sign / mid_price
          spread = spread if spread > 0.0 else 0.0
          if numpy.isnan(spread):
            continue
          elif numpy.isnan(min_spread):
            min_spread = spread
          else:
            min_spread = min(min_spread, spread)

      if ts_id_type_array[i, 0] == ts_array[ts_idx]:
        res[ts_idx] = min_spread
      else:
        ts_idx += 1
        res[ts_idx] = min_spread

  print("MM stat calculation starts.....")
  cpdf = oedf.copy()

  copied_bbodf = bbodf.copy()
  copied_bbodf.drop_duplicates(inplace=True)
  copied_bbodf[
      'timestamp'] = copied_bbodf['book_timestamp'] if is_pnl_plot else copied_bbodf['timestamp']

  cpdf.loc[cpdf['sign'] == 1.0, 'sign'] = order_enums.BUY_ORDER
  cpdf.loc[cpdf['sign'] == -1.0, 'sign'] = order_enums.SELL_ORDER

  price_bp_tholds = [75.0, 300.0] if not price_bp_tholds else [float(i) for i in price_bp_tholds]
  quote_tholds = [100] if not quote_tholds else [float(i) for i in quote_tholds]
  base_tholds = [None] if not base_tholds else [float(i) for i in base_tholds]
  market_type, exchange, api_ver = mea.split('.')
  product = generate_product_from_str2(
      market_type, exchange, api_ver, symbol,
      pandas.DatetimeIndex([copied_bbodf['timestamp'].min()]).to_pydatetime()[0])
  pi = product.product_holder.product_info

  final_res = pandas.DataFrame(columns=[
      'ts', 'uptime_res_list', 'tight_uptime_res_list', 'sell_spreads_list', 'buy_spreads_list',
      'tight_sell_depth_list', 'tight_buy_depth_list', 'sell_depth_list', 'buy_depth_list'
  ])
  if len(base_tholds) == 1 and base_tholds[0] is None:
    final_res.index = pandas.MultiIndex(levels=[[], []],
                                        codes=[[], []],
                                        names=['price_bp_thold', 'quote_thold'])
  else:
    final_res.index = pandas.MultiIndex(levels=[[], [], []],
                                        codes=[[], [], []],
                                        names=['price_bp_thold', 'quote_thold', 'base_thold'])
  final_res = final_res.astype(object)

  exclude_ids = cpdf.loc[cpdf['type'].isin(['ORDER_ERROR', 'ORDER_AUTO_CANCELED']),
                         'order_id'].unique()
  cpdf = cpdf.loc[~cpdf['order_id'].isin(exclude_ids)]
  wodf = cpdf.loc[cpdf['type'].isin(['ORDER_ACCEPTED', 'ORDER_FILLED', 'CANCEL_CONFIRMED']), :]
  type_map = {'ORDER_ACCEPTED': 0, 'ORDER_FILLED': 1, 'CANCEL_CONFIRMED': 2}
  wodf.loc[:, 'type'] = wodf['type'].map(type_map)
  wodf = wodf.sort_values(by=['timestamp', 'order_id', 'type'], ascending=[True, True, True]).copy()
  # With pnl_plot -> Delete previously Accepted Orders (Can't know how much the left_qty is)
  if is_pnl_plot:
    tmp = wodf.groupby('order_id').first()
    not_accepted_order_ids = tmp.loc[tmp['type'] != 0].index.to_numpy()
    wodf = wodf.loc[~wodf['order_id'].isin(not_accepted_order_ids)]
  # Check edge case with type e.g. CANCEL_CONFIRMED comes faster than ORDER_FILLED
  type_sorted_tmp = wodf.sort_values(['order_id', 'type', 'timestamp'])
  ts_sorted_tmp = wodf[['order_id', 'timestamp']].sort_values(['order_id', 'timestamp'])
  oe_sorted_wodf = type_sorted_tmp.copy()
  oe_sorted_wodf['timestamp'] = ts_sorted_tmp['timestamp'].to_numpy()
  oe_sorted_wodf.sort_values(['timestamp', 'order_id', 'type'], inplace=True)
  final_wodf = oe_sorted_wodf.merge(copied_bbodf[['timestamp', 'mid_price']].drop_duplicates(),
                                    on='timestamp',
                                    how='outer').sort_values('timestamp')
  final_wodf['mid_price'].ffill(inplace=True)
  final_wodf['type'].fillna(3, inplace=True)
  final_wodf['order_id'].fillna(0, inplace=True)
  final_wodf['sign'].fillna(0.0, inplace=True)
  final_wodf.reset_index(drop=True, inplace=True)
  buy_wodf = final_wodf.loc[
      final_wodf['sign'].isin([0.0, order_enums.BUY_ORDER]),
      ['timestamp', 'order_id', 'type', 'order_price', 'order_qty', 'fill_qty', 'mid_price'
      ]].sort_values(['timestamp', 'order_id', 'type'], ascending=[True, True, True])
  sell_wodf = final_wodf.loc[
      final_wodf['sign'].isin([0.0, order_enums.SELL_ORDER]),
      ['timestamp', 'order_id', 'type', 'order_price', 'order_qty', 'fill_qty', 'mid_price'
      ]].sort_values(['timestamp', 'order_id', 'type'], ascending=[True, True, True])
  buy_depth_arr = None
  sell_depth_arr = None
  buy_ts_array = None
  sell_ts_array = None

  if buy_wodf.shape[0] > 0:
    buy_wodf['price_thold'] = 0.0
    buy_wo_array = buy_wodf[['order_qty', 'order_price', 'fill_qty', 'price_thold']].to_numpy()
    buy_ts_id_type_array = buy_wodf[['timestamp', 'order_id',
                                     'type']].astype(numpy.int64).to_numpy()
    buy_ts_array = numpy.unique(buy_ts_id_type_array[:, 0])
    buy_depth_arr = get_depth(buy_wo_array, buy_ts_id_type_array, buy_ts_array, 1, 0.0)

  if sell_wodf.shape[0] > 0:
    sell_wodf['price_thold'] = numpy.inf
    sell_wo_array = sell_wodf[['order_qty', 'order_price', 'fill_qty', 'price_thold']].to_numpy()
    sell_ts_id_type_array = sell_wodf[['timestamp', 'order_id',
                                       'type']].astype(numpy.int64).to_numpy()
    sell_ts_array = numpy.unique(sell_ts_id_type_array[:, 0])
    sell_depth_arr = get_depth(sell_wo_array, sell_ts_id_type_array, sell_ts_array, -1, 0.0)
  # depth calculation
  buy_depth_sr = pandas.Series(buy_depth_arr, index=buy_ts_array, name='buy_depth')
  sell_depth_sr = pandas.Series(sell_depth_arr, index=sell_ts_array, name='sell_depth')
  depth_df = pandas.concat([buy_depth_sr, sell_depth_sr], axis=1).sort_index().ffill().fillna(0)

  for price_bp_thold, quote_thold, base_thold in itertools.product(price_bp_tholds, quote_tholds,
                                                                   base_tholds):
    tight_buy_wodf = final_wodf.loc[
        final_wodf['sign'].isin([0.0, order_enums.BUY_ORDER]),
        ['timestamp', 'order_id', 'type', 'order_price', 'order_qty', 'mid_price', 'fill_qty'
        ]].sort_values(['timestamp', 'order_id', 'type'], ascending=[True, True, True])
    tight_sell_wodf = final_wodf.loc[
        final_wodf['sign'].isin([0.0, order_enums.SELL_ORDER]),
        ['timestamp', 'order_id', 'type', 'order_price', 'order_qty', 'mid_price', 'fill_qty'
        ]].sort_values(['timestamp', 'order_id', 'type'], ascending=[True, True, True])
    tight_buy_depth_arr = None
    tight_sell_depth_arr = None
    tight_buy_ts_array = None
    tight_sell_ts_array = None
    buy_spread_arr = None
    sell_spread_arr = None
    if tight_buy_wodf.shape[0] > 0:
      tight_buy_wodf['price_thold'] = (tight_buy_wodf['mid_price'] *
                                       (1 - EPS)).apply(pi.round_down) * (1 - price_bp_thold * 1e-4)
      tight_buy_wo_array = tight_buy_wodf[['order_qty', 'order_price', 'fill_qty',
                                           'price_thold']].to_numpy()
      tight_buy_ts_id_array = tight_buy_wodf[['timestamp', 'order_id',
                                              'type']].astype(numpy.int64).to_numpy()
      tight_buy_ts_array = numpy.unique(tight_buy_ts_id_array[:, 0])
      tight_buy_depth_arr = get_depth(tight_buy_wo_array, tight_buy_ts_id_array, tight_buy_ts_array,
                                      1, quote_thold)
      tight_buy_wo_array = tight_buy_wodf[[
          'order_qty', 'order_price', 'fill_qty', 'price_thold', 'mid_price'
      ]].to_numpy()
      buy_spread_arr = get_spread(tight_buy_wo_array, tight_buy_ts_id_array, tight_buy_ts_array, 1,
                                  quote_thold)

    if tight_sell_wodf.shape[0] > 0:
      tight_sell_wodf['price_thold'] = (tight_sell_wodf['mid_price'] *
                                        (1 + EPS)).apply(pi.round_up) * (1 + price_bp_thold * 1e-4)
      tight_sell_wo_array = tight_sell_wodf[['order_qty', 'order_price', 'fill_qty',
                                             'price_thold']].to_numpy()
      tight_sell_ts_id_array = tight_sell_wodf[['timestamp', 'order_id',
                                                'type']].astype(numpy.int64).to_numpy()
      tight_sell_ts_array = numpy.unique(tight_sell_ts_id_array[:, 0])
      tight_sell_depth_arr = get_depth(tight_sell_wo_array, tight_sell_ts_id_array,
                                       tight_sell_ts_array, -1, quote_thold)
      tight_sell_wo_array = tight_sell_wodf[[
          'order_qty', 'order_price', 'fill_qty', 'price_thold', 'mid_price'
      ]].to_numpy()
      sell_spread_arr = get_spread(tight_sell_wo_array, tight_sell_ts_id_array, tight_sell_ts_array,
                                   -1, quote_thold)

    # tight depth calculation
    tight_buy_depth_sr = pandas.Series(tight_buy_depth_arr,
                                       index=tight_buy_ts_array,
                                       name='tight_buy_depth')
    tight_sell_depth_sr = pandas.Series(tight_sell_depth_arr,
                                        index=tight_sell_ts_array,
                                        name='tight_sell_depth')
    tight_depth_df = pandas.concat([tight_buy_depth_sr, tight_sell_depth_sr],
                                   axis=1).sort_index().ffill().fillna(0)

    # uptime calculation
    uptime_sr = (depth_df['buy_depth'] > 0.0) | (depth_df['sell_depth'] > 0.0)
    tight_uptime_sr = (tight_depth_df['tight_buy_depth'] >
                       0.0) | (tight_depth_df['tight_sell_depth'] > 0.0)
    # spread calculation
    buy_spread_sr = pandas.Series(buy_spread_arr, index=buy_ts_array,
                                  name='buy_spread').fillna(numpy.inf)
    sell_spread_sr = pandas.Series(sell_spread_arr, index=sell_ts_array,
                                   name='sell_spread').fillna(numpy.inf)
    spread_df = pandas.concat([buy_spread_sr, sell_spread_sr], axis=1).sort_index().ffill()
    spread_df[spread_df == numpy.inf] = numpy.nan

    ts_list = uptime_sr.index.tolist()
    uptime_ratio = uptime_sr.sum() / len(uptime_sr)
    tight_uptime_ratio = tight_uptime_sr.sum() / len(tight_uptime_sr)
    buy_avg_depth, sell_avg_depth = depth_df[depth_df > 0.0].mean().to_numpy()
    tight_buy_avg_depth, tight_sell_avg_depth = tight_depth_df[
        tight_depth_df > 0.0].mean().to_numpy()
    buy_avg_spread, sell_avg_spread = spread_df[spread_df < 1].mean().to_numpy()
    uptime_res_arr = uptime_sr.to_list()
    tight_uptime_res_arr = tight_uptime_sr.to_list()
    tight_buy_depth_arr = tight_depth_df['tight_buy_depth'].to_list()
    tight_sell_depth_arr = tight_depth_df['tight_sell_depth'].to_list()
    buy_depth_arr = depth_df['buy_depth'].to_list()
    sell_depth_arr = depth_df['sell_depth'].to_list()
    buy_spread_arr = spread_df['buy_spread'].to_list()
    sell_spread_arr = spread_df['sell_spread'].to_list()
    # print('===============================================================')
    # print(f'price_bp_thold: {price_bp_thold}, quote_thold: {quote_thold}, base_thold: {base_thold}')
    # print(f'uptime: {100 * uptime_ratio:.2f}%' if uptime_ratio else 'uptime: N/A')
    # print(f'tight uptime: {100 * tight_uptime_ratio:.2f}%'
    #       if tight_uptime_ratio else 'tight uptime: N/A')
    # print(f'sell_avg_spread: {sell_avg_spread:,.2%}, buy_avg_spread: {buy_avg_spread:,.2%}'
    #       if buy_avg_spread and sell_avg_spread else
    #       'sell_avg_spread: N/A, buy_avg_spread: N/A' if not buy_avg_spread and not sell_avg_spread
    #       else f'sell_avg_spread: {sell_avg_spread:,.2%}, buy_avg_spread: N/A'
    #       if not buy_avg_spread else f'sell_avg_spread: N/A, buy_avg_spread: {buy_avg_spread:,.2%}')
    # print('tight_sell_avg_depth: {:,.2f}, tight_buy_avg_depth: {:,.2f}'.format(
    #     tight_sell_avg_depth, tight_buy_avg_depth))
    # print('sell_avg_depth: {:,.2f}, buy_avg_depth: {:,.2f}'.format(sell_avg_depth, buy_avg_depth))
    # print('===============================================================')
    array = (ts_list, uptime_res_arr, tight_uptime_res_arr, sell_spread_arr, buy_spread_arr,
             tight_sell_depth_arr, tight_buy_depth_arr, sell_depth_arr, buy_depth_arr)

    assert len(final_res.columns) == len(array)
    if base_thold is not None:
      final_res.at[(price_bp_thold, quote_thold, base_thold), :] = numpy.nan
      final_res.at[(price_bp_thold, quote_thold, base_thold), :] = array
    else:
      final_res.at[(price_bp_thold, quote_thold), :] = numpy.nan
      final_res.at[(price_bp_thold, quote_thold), :] = array

  return final_res


def plot_order_plot(filename_prefix,
                    start_time,
                    end_time,
                    oedf,
                    bbodf,
                    pnlts,
                    pnls,
                    posts,
                    poss,
                    prcdict,
                    plot_dir="order_plot",
                    active_pnl=None,
                    plot_tags=None):
  os.makedirs(plot_dir, exist_ok=True)
  filename = os.path.join(plot_dir, "ordplt_%s.png" % filename_prefix)
  log_filename = os.path.join(plot_dir, "ordplt_%s.txt" % filename_prefix)
  start_ts, end_ts = pandas.DatetimeIndex([start_time, end_time]).astype(int)

  pnlti = pandas.DatetimeIndex(pnlts)
  pidx = (start_ts <= pnlti.astype(int)) & (pnlti.astype(int) <= end_ts)

  if pidx.sum() == 0:
    return

  print("\nProcessing %s ..." % filename_prefix)

  plt.rcParams['figure.figsize'] = 10, 8
  plt.rcParams['font.size'] = 6
  plt.rcParams['legend.fontsize'] = 6
  plt.rcParams['xtick.labelsize'] = 6
  plt.rcParams['ytick.labelsize'] = 6
  plot_num = 3
  plt.subplot(plot_num, 1, 1)
  plt.title(f"{filename_prefix}")
  plot_ordpos(start_ts, end_ts, bbodf, posts, poss, prcdict)

  plt.subplot(plot_num, 1, 2)
  plot_edges(oedf, bbodf, start_time, end_time)
  set_xlim_time(start_time, end_time)

  plt.subplot(plot_num, 1, 3)
  start_ts, end_ts = set_xlim_time(start_time, end_time)

  plt.plot(pnlti[pidx].to_numpy(), pnls[pidx].to_numpy(), 'k-', drawstyle='steps-post', lw=0.5)
  pnl_legend = ['pnl']
  if active_pnl is not None:
    plt.plot(pnlti[pidx].to_numpy(),
             active_pnl[pidx].to_numpy(),
             'r-',
             drawstyle='steps-post',
             lw=0.5)
    pnl_legend.append('active_pnl')
  plt.legend(pnl_legend, loc='upper left')
  plt.twinx()
  plt.plot(pandas.DatetimeIndex(posts).to_numpy(),
           poss.to_numpy(),
           lw=0.5,
           label="position(consolidated)",
           drawstyle='steps-post')
  plt.ylabel("position")
  plt.legend(loc="upper right")

  plt.savefig(filename, dpi=250)
  plt.close()

  with open(log_filename, 'w') as outfile:
    oeti = pandas.DatetimeIndex(oedf['timestamp'])
    oeidx = (start_ts <= oeti) & (oeti <= end_ts)
    debug_df = sscalc2.get_debug_df(oedf.loc[oeidx], live=True)
    print(sscalc2.get_tabulate_str(debug_df), file=outfile)

  if plot_tags:
    filename = os.path.join(plot_dir, "ordplt_%s_tagplt.png" % filename_prefix)
    exnulloedf = oedf[~oedf['order_tag'].isnull()]
    tags = exnulloedf['order_tag'].unique()
    plot_num = 2 * len(tags) + 1
    plt.rcParams['figure.figsize'] = 10, 8 * plot_num / 3.0
    plt.rcParams['font.size'] = 6
    plt.rcParams['legend.fontsize'] = 6
    plt.rcParams['xtick.labelsize'] = 6
    plt.rcParams['ytick.labelsize'] = 6
    for idx, (tag, tmpdf) in enumerate(exnulloedf.groupby(['order_tag'])):
      start_ts, end_ts = pandas.DatetimeIndex([start_time, end_time]).astype(int)
      plt.subplot(plot_num, 1, 2 * idx + 1)
      plot_ordpos(start_ts, end_ts, bbodf, posts, poss, prcdict, tag)
      plt.title(f"{filename_prefix} TAG: {tag}")
      plt.subplot(plot_num, 1, 2 * idx + 2)
      plot_edges(tmpdf, bbodf, start_time, end_time)
      set_xlim_time(start_time, end_time)
    exnulloedf['order_amt'] = exnulloedf['order_qty'] * exnulloedf['order_price']
    exnulloedf['fill_amt'] = exnulloedf['fill_qty'] * exnulloedf['order_price']
    # fill information
    filldf = exnulloedf.groupby(['order_tag', 'sign'])['fill_amt', 'fill_qty'].sum()
    filldf['avg_price'] = filldf['fill_amt'] / filldf['fill_qty']
    subdf = exnulloedf[exnulloedf['type'] == "ORDER_ACCEPTED"].groupby(['order_tag', 'sign'])[
        'order_amt'].sum()
    subdf.name = 'sub_amt'
    fillid = exnulloedf.loc[(exnulloedf['type'] == "ORDER_FILLED") & (exnulloedf['fill_qty'] > 0),
                            'order_id'].unique()
    filled_subdf = exnulloedf.loc[(exnulloedf['order_id'].isin(fillid)) &
                                  (exnulloedf["type"] == "ORDER_ACCEPTED")].groupby(
                                      ['order_tag', 'sign'])['order_amt'].sum()
    filled_subdf.name = 'filled_sub_amt'
    frdf = filldf['fill_amt'] / subdf
    frdf.name = 'fr'
    ffrdf = filldf['fill_amt'] / filled_subdf
    ffrdf.name = 'ffr'
    fill_cnt = exnulloedf[exnulloedf['type'] == "ORDER_FILLED"].groupby(['order_tag', 'sign'
                                                                        ])['order_id'].count()
    fill_cnt.name = 'fill_cnt'
    acc_cnt = exnulloedf[exnulloedf['type'] == "ORDER_ACCEPTED"].groupby(['order_tag', 'sign'
                                                                         ])['order_id'].count()
    acc_cnt.name = 'sub_cnt'
    finaldf = pandas.concat([filldf, subdf, filled_subdf, frdf, ffrdf, fill_cnt, acc_cnt], axis=1)
    # make finaldf to matplotlib table with background off
    finaldf = finaldf.reset_index()
    finaldf = finaldf.fillna(0)
    finaldf['sign'] = finaldf['sign'].map({1: 'BUY', -1: 'SELL'})
    finaldf['fr'] = finaldf['fr'].apply(lambda x: f"{x:.2%}")
    finaldf['ffr'] = finaldf['ffr'].apply(lambda x: f"{x:.2%}")
    finaldf['avg_price'] = finaldf['avg_price'].apply(lambda x: f"{x:.8f}")
    finaldf['fill_amt'] = finaldf['fill_amt'].apply(lambda x: f"{x:,.0f}")
    finaldf['fill_qty'] = finaldf['fill_qty'].apply(lambda x: f"{x:,.8f}")
    finaldf['sub_amt'] = finaldf['sub_amt'].apply(lambda x: f"{x:,.0f}")
    finaldf['filled_sub_amt'] = finaldf['filled_sub_amt'].apply(lambda x: f"{x:,.0f}")
    finaldf['fill_cnt'] = finaldf['fill_cnt'].apply(lambda x: f"{x:,.0f}")
    finaldf['sub_cnt'] = finaldf['sub_cnt'].apply(lambda x: f"{x:,.0f}")
    finaldf = finaldf.reindex(columns=[
        'order_tag', 'sign', 'fill_amt', 'fill_qty', 'avg_price', 'sub_amt', 'filled_sub_amt', 'fr',
        'ffr', 'fill_cnt', 'sub_cnt'
    ])
    ax = plt.subplot(plot_num, 1, plot_num)
    table = ax.table(cellText=finaldf.values,
                     colLabels=finaldf.columns,
                     loc='center',
                     cellLoc='center')
    table.auto_set_font_size(False)
    table.set_fontsize(6)
    table.auto_set_column_width(col=list(range(finaldf.shape[1])))
    ax.axis('off')
    plt.tight_layout()
    plt.savefig(filename, dpi=250)
    plt.close()
